package com.chen.gui.component.list;

import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Insets;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.List;

import javax.swing.BorderFactory;
import javax.swing.CellRendererPane;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JScrollPane;
import javax.swing.JTextField;
import javax.swing.JViewport;
import javax.swing.Scrollable;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
import javax.swing.ToolTipManager;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.event.MouseInputAdapter;
import javax.swing.text.JTextComponent;

import com.chen.Shell;

public class CList0 extends JComponent implements Scrollable {

	private static final long serialVersionUID = -3030213190904043677L;

	private int visibleRowCount = 8;

	private ListCellRenderer cellRenderer = new DefaultListCellRenderer();
	private List<Object> data = new ArrayList<Object>();

	private Handler handler = new Handler();

	private int cellHeight = 26;

	private int selectedIndex = -1;

	private int mouseOnIndex = -1;

	private double ratio = 1.5;

	private CellRendererPane crp = new CellRendererPane();

	private int editingIndex = -1;

	private boolean editable = true;

	public static void main(String[] args) {
		CList0 list = new CList0();
		list.addElement("hehe");
		list.addElement("hehe");
		list.addElement("hehe");
		list.addElement("hehe");
		list.addElement("hehe");
		list.addElement("hehe");
		list.addElement("hehe");
		Shell.run(new JScrollPane(list));
	}

	public CList0() {
		ToolTipManager toolTipManager = ToolTipManager.sharedInstance();
		toolTipManager.registerComponent(this);

		setAutoscrolls(true);
		setOpaque(true);

		this.setBorder(BorderFactory.createEtchedBorder());
		this.addMouseListener(handler);
		this.addMouseMotionListener(handler);
	}

	public void addElement(Object item) {
		if (item == null)
			return;

		data.add(item);
		repaint();
	}

	public Object getElementAt(int index) {
		return data.get(index);
	}

	public boolean isSelected(int index) {
		return index == selectedIndex;
	}

	public boolean isMouseOn(int index) {
		return index == mouseOnIndex;
	}

	public boolean isEditable() {
		return editable;
	}

	public void setEditable(boolean editable) {
		this.editable = editable;
	}

	@Override
	protected void paintComponent(Graphics g) {
		for (int i = 0; i < data.size(); i++) {
			Component c = cellRenderer.getListCellRendererComponent(this, i);
			Rectangle rect = getCellBounds(i);

			if (c instanceof JTextComponent) {
				rect = new Rectangle(rect.x, (int) (rect.y + (ratio - 1) * cellHeight / 2), rect.width,
						cellHeight);
			}

			crp.paintComponent(g, c, this, rect);
		}
	}

	public ListCellRenderer getCellRenderer() {
		return cellRenderer;
	}

	public void setCellRenderer(ListCellRenderer cellRenderer) {
		if (cellRenderer == null)
			return;

		this.cellRenderer = cellRenderer;
		repaint();
	}

	public int getVisibleRowCount() {
		return visibleRowCount;
	}

	public void setVisibleRowCount(int visibleRowCount) {
		int oldValue = this.visibleRowCount;
		this.visibleRowCount = Math.max(0, visibleRowCount);
		firePropertyChange("visibleRowCount", oldValue, visibleRowCount);
	}

	private void stopEdit() {
		if (isEditable() && editingIndex > -1) {
			Component c = cellRenderer.getListCellRendererComponent(this, editingIndex);
			JTextField tf = (JTextField) c;
			data.set(editingIndex, tf.getText());
			editingIndex = -1;
			removeAll();
			requestFocus();
			repaint();
		}
	}

	public int getFirstVisibleIndex() {
		Rectangle r = getVisibleRect();
		int first;
		if (this.getComponentOrientation().isLeftToRight()) {
			first = locationToIndex(r.getLocation());
		} else {
			first = locationToIndex(new Point((r.x + r.width) - 1, r.y));
		}
		if (first != -1) {
			Rectangle bounds = getCellBounds(first);
			if (bounds != null) {
				SwingUtilities.computeIntersection(r.x, r.y, r.width, r.height, bounds);
				if (bounds.width == 0 || bounds.height == 0) {
					first = -1;
				}
			}
		}
		return first;
	}

	public void ensureIndexIsVisible(int index) {
		Rectangle cellBounds = getCellBounds(index);
		if (cellBounds != null) {
			scrollRectToVisible(cellBounds);
		}
	}

	@Override
	public String getToolTipText(MouseEvent event) {
		if (event != null) {
			Point p = event.getPoint();
			int index = locationToIndex(p);
			ListCellRenderer r = getCellRenderer();
			Rectangle cellBounds;

			if (index != -1 && r != null && (cellBounds = getCellBounds(index)) != null
					&& cellBounds.contains(p.x, p.y)) {
				Component rComponent = r.getListCellRendererComponent(this, index);

				if (rComponent instanceof JComponent) {
					MouseEvent newEvent;

					p.translate(-cellBounds.x, -cellBounds.y);
					newEvent = new MouseEvent(rComponent, event.getID(), event.getWhen(),
							event.getModifiers(), p.x, p.y, event.getXOnScreen(), event.getYOnScreen(),
							event.getClickCount(), event.isPopupTrigger(), MouseEvent.NOBUTTON);

					String tip = ((JComponent) rComponent).getToolTipText(newEvent);

					if (tip != null) {
						return tip;
					}
				}
			}
		}
		return super.getToolTipText();
	}

	public int locationToIndex(Point location) {
		for (int i = 0; i < data.size(); i++) {
			if (getCellBounds(i).contains(location))
				return i;
		}

		return -1;
	}

	public Point indexToLocation(int index) {
		return null;
	}

	public Rectangle getCellBounds(int index) {
		Insets insets = getInsets();
		int x = insets.left;
		int y = cellHeight * index;
		if (mouseOnIndex > -1) {
			if (mouseOnIndex >= index) {
				y = cellHeight * index;
			} else {
				y = (int) ((index - 1) * cellHeight + ratio * cellHeight);
			}
		}

		int w = getWidth() - x - insets.right;
		int h = index == mouseOnIndex ? (int) (cellHeight * ratio) : cellHeight;

		return new Rectangle(x, y, w, h);
	}

	protected void fireSelectionValueChanged(int firstIndex, int lastIndex, boolean isAdjusting) {
		Object[] listeners = listenerList.getListenerList();
		ListSelectionEvent e = null;

		for (int i = listeners.length - 2; i >= 0; i -= 2) {
			if (listeners[i] == ListSelectionListener.class) {
				if (e == null) {
					e = new ListSelectionEvent(this, firstIndex, lastIndex, isAdjusting);
				}
				((ListSelectionListener) listeners[i + 1]).valueChanged(e);
			}
		}
	}

	public void addListSelectionListener(ListSelectionListener listener) {
		listenerList.add(ListSelectionListener.class, listener);
	}

	public void removeListSelectionListener(ListSelectionListener listener) {
		listenerList.remove(ListSelectionListener.class, listener);
	}

	public ListSelectionListener[] getListSelectionListeners() {
		return listenerList.getListeners(ListSelectionListener.class);
	}

	public void setSelectedIndex(int index) {
		if (index >= data.size())
			return;

		selectedIndex = index;
		repaint();
	}

	public int getSelectedIndex() {
		return selectedIndex;
	}

	public Object getSelectedValue() {
		return (selectedIndex == -1) ? null : getElementAt(selectedIndex);
	}

	public void setSelectedValue(Object anObject, boolean shouldScroll) {
		if (anObject == null)
			setSelectedIndex(-1);
		else if (!anObject.equals(getSelectedValue())) {
			int i, c;
			for (i = 0, c = data.size(); i < c; i++)
				if (anObject.equals(getElementAt(i))) {
					setSelectedIndex(i);
					if (shouldScroll)
						ensureIndexIsVisible(i);
					repaint();
					return;
				}
			setSelectedIndex(-1);
		}
		repaint();
	}

	private void checkScrollableParameters(Rectangle visibleRect, int orientation) {
		if (visibleRect == null) {
			throw new IllegalArgumentException("visibleRect must be non-null");
		}
		switch (orientation) {
		case SwingConstants.VERTICAL:
		case SwingConstants.HORIZONTAL:
			break;
		default:
			throw new IllegalArgumentException("orientation must be one of: VERTICAL, HORIZONTAL");
		}
	}

	@Override
	public Dimension getPreferredSize() {
		Insets insets = getInsets();
		int visibleRowCount = getVisibleRowCount();
		int width = getBounds().width;

		Rectangle r = getCellBounds(0);
		int h = visibleRowCount * r.height;
		if (mouseOnIndex != -1) {
			h += cellHeight * (ratio - 1);
		}

		int height = h + insets.top + insets.bottom;
		return new Dimension(width, height);
	}

	@Override
	public Dimension getPreferredScrollableViewportSize() {
		Insets insets = getInsets();
		int visibleRowCount = getVisibleRowCount();
		int width = getPreferredSize().width;

		int h = visibleRowCount * cellHeight;
		if (mouseOnIndex != -1) {
			h += cellHeight * (ratio - 1);
		}

		int height = h + insets.top + insets.bottom;
		return new Dimension(width, height);
	}

	@Override
	public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) {
		checkScrollableParameters(visibleRect, orientation);

		if (orientation == SwingConstants.VERTICAL) {
			int row = locationToIndex(visibleRect.getLocation());

			if (row == -1) {
				return 0;
			} else {
				/* Scroll Down */
				if (direction > 0) {
					Rectangle r = getCellBounds(row);
					return (r == null) ? 0 : r.height - (visibleRect.y - r.y);
				}
				/* Scroll Up */
				else {
					Rectangle r = getCellBounds(row);

					/*
					 * The first row is completely visible and it's row 0. We're
					 * done.
					 */
					if ((r.y == visibleRect.y) && (row == 0)) {
						return 0;
					}
					/*
					 * The first row is completely visible, return the height of
					 * the previous row or 0 if the first row is the top row of
					 * the list.
					 */
					else if (r.y == visibleRect.y) {
						Point loc = r.getLocation();
						loc.y--;
						int prevIndex = locationToIndex(loc);
						Rectangle prevR = getCellBounds(prevIndex);

						if (prevR == null || prevR.y >= r.y) {
							return 0;
						}
						return prevR.height;
					}
					/*
					 * The first row is partially visible, return the height of
					 * hidden part.
					 */
					else {
						return visibleRect.y - r.y;
					}
				}
			}
		} else if (orientation == SwingConstants.HORIZONTAL) {
			boolean leftToRight = getComponentOrientation().isLeftToRight();
			int index;
			Point leadingPoint;

			if (leftToRight) {
				leadingPoint = visibleRect.getLocation();
			} else {
				leadingPoint = new Point(visibleRect.x + visibleRect.width - 1, visibleRect.y);
			}
			index = locationToIndex(leadingPoint);

			if (index != -1) {
				Rectangle cellBounds = getCellBounds(index);
				if (cellBounds != null && cellBounds.contains(leadingPoint)) {
					int leadingVisibleEdge;
					int leadingCellEdge;

					if (leftToRight) {
						leadingVisibleEdge = visibleRect.x;
						leadingCellEdge = cellBounds.x;
					} else {
						leadingVisibleEdge = visibleRect.x + visibleRect.width;
						leadingCellEdge = cellBounds.x + cellBounds.width;
					}

					if (leadingCellEdge != leadingVisibleEdge) {
						if (direction < 0) {
							// Show remainder of leading cell
							return Math.abs(leadingVisibleEdge - leadingCellEdge);

						} else if (leftToRight) {
							// Hide rest of leading cell
							return leadingCellEdge + cellBounds.width - leadingVisibleEdge;
						} else {
							// Hide rest of leading cell
							return leadingVisibleEdge - cellBounds.x;
						}
					}
					// ASSUME: All cells are the same width
					return cellBounds.width;
				}
			}
		}
		Font f = getFont();
		return (f != null) ? f.getSize() : 1;
	}

	@Override
	public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) {
		checkScrollableParameters(visibleRect, orientation);
		if (orientation == SwingConstants.VERTICAL) {
			int inc = visibleRect.height;
			/* Scroll Down */
			if (direction > 0) {
				// last cell is the lowest left cell
				int last = locationToIndex(new Point(visibleRect.x, visibleRect.y + visibleRect.height - 1));
				if (last != -1) {
					Rectangle lastRect = getCellBounds(last);
					if (lastRect != null) {
						inc = lastRect.y - visibleRect.y;
						if ((inc == 0) && (last < data.size() - 1)) {
							inc = lastRect.height;
						}
					}
				}
			}
			/* Scroll Up */
			else {
				int newFirst = locationToIndex(new Point(visibleRect.x, visibleRect.y - visibleRect.height));
				int first = getFirstVisibleIndex();
				if (newFirst != -1) {
					if (first == -1) {
						first = locationToIndex(visibleRect.getLocation());
					}
					Rectangle newFirstRect = getCellBounds(newFirst);
					Rectangle firstRect = getCellBounds(first);
					if ((newFirstRect != null) && (firstRect != null)) {
						while ((newFirstRect.y + visibleRect.height < firstRect.y + firstRect.height)
								&& (newFirstRect.y < firstRect.y)) {
							newFirst++;
							newFirstRect = getCellBounds(newFirst);
						}
						inc = visibleRect.y - newFirstRect.y;
						if ((inc <= 0) && (newFirstRect.y > 0)) {
							newFirst--;
							newFirstRect = getCellBounds(newFirst);
							if (newFirstRect != null) {
								inc = visibleRect.y - newFirstRect.y;
							}
						}
					}
				}
			}
			return inc;
		} else if (orientation == SwingConstants.HORIZONTAL) {
			boolean leftToRight = getComponentOrientation().isLeftToRight();
			int inc = visibleRect.width;
			/* Scroll Right (in ltr mode) or Scroll Left (in rtl mode) */
			if (direction > 0) {
				// position is upper right if ltr, or upper left otherwise
				int x = visibleRect.x + (leftToRight ? (visibleRect.width - 1) : 0);
				int last = locationToIndex(new Point(x, visibleRect.y));

				if (last != -1) {
					Rectangle lastRect = getCellBounds(last);
					if (lastRect != null) {
						if (leftToRight) {
							inc = lastRect.x - visibleRect.x;
						} else {
							inc = visibleRect.x + visibleRect.width - (lastRect.x + lastRect.width);
						}
						if (inc < 0) {
							inc += lastRect.width;
						} else if ((inc == 0) && (last < data.size() - 1)) {
							inc = lastRect.width;
						}
					}
				}
			}
			/* Scroll Left (in ltr mode) or Scroll Right (in rtl mode) */
			else {
				// position is upper left corner of the visibleRect shifted
				// left by the visibleRect.width if ltr, or upper right shifted
				// right by the visibleRect.width otherwise
				int x = visibleRect.x
						+ (leftToRight ? -visibleRect.width : visibleRect.width - 1 + visibleRect.width);
				int first = locationToIndex(new Point(x, visibleRect.y));

				if (first != -1) {
					Rectangle firstRect = getCellBounds(first);
					if (firstRect != null) {
						// the right of the first cell
						int firstRight = firstRect.x + firstRect.width;

						if (leftToRight) {
							if ((firstRect.x < visibleRect.x - visibleRect.width)
									&& (firstRight < visibleRect.x)) {
								inc = visibleRect.x - firstRight;
							} else {
								inc = visibleRect.x - firstRect.x;
							}
						} else {
							int visibleRight = visibleRect.x + visibleRect.width;

							if ((firstRight > visibleRight + visibleRect.width)
									&& (firstRect.x > visibleRight)) {
								inc = firstRect.x - visibleRight;
							} else {
								inc = firstRight - visibleRight;
							}
						}
					}
				}
			}
			return inc;
		}
		return visibleRect.width;
	}

	@Override
	public boolean getScrollableTracksViewportWidth() {
		if (getVisibleRowCount() <= 0) {
			return true;
		}
		if (getParent() instanceof JViewport) {
			return (((JViewport) getParent()).getWidth() > getPreferredSize().width);
		}
		return false;
	}

	@Override
	public boolean getScrollableTracksViewportHeight() {
		if (getVisibleRowCount() <= 0) {
			return true;
		}
		if (getParent() instanceof JViewport) {
			return (((JViewport) getParent()).getHeight() > getPreferredSize().height);
		}
		return false;
	}

	class Handler extends MouseInputAdapter implements ListSelectionListener, ActionListener {

		@Override
		public void mouseClicked(MouseEvent e) {
		}

		@Override
		public void mousePressed(MouseEvent e) {
		}

		@Override
		public void mouseReleased(MouseEvent e) {
			if (e.getButton() == MouseEvent.BUTTON1) {
				int index = locationToIndex(e.getPoint());
				if (index > -1) {
					if (isEditable() && index == selectedIndex) {
						editingIndex = index;
						JTextField tf = (JTextField) getCellRenderer().getListCellRendererComponent(
								CList0.this, index);

						CList0.this.add(tf);
						Rectangle cellBounds = getCellBounds(index);
						Rectangle tfBounds = new Rectangle(cellBounds.x, (int) (cellBounds.y + (ratio - 1)
								* cellHeight / 2), cellBounds.width, cellHeight);

						tf.setBounds(tfBounds);
						tf.setSelectionStart(0);
						tf.setSelectionEnd(tf.getText().length());
						return;
					} else {
						setSelectedIndex(index);
					}
				}
				stopEdit();
			}
		}

		@Override
		public void mouseEntered(MouseEvent e) {
		}

		@Override
		public void mouseExited(MouseEvent e) {
		}

		@Override
		public void mouseMoved(MouseEvent e) {
			Point point = e.getPoint();
			int index = locationToIndex(point);
			if (getCellBounds(index).contains(point)) {
				mouseOnIndex = index;
			}

			repaint();
		}

		@Override
		public void valueChanged(ListSelectionEvent e) {
		}

		@Override
		public void actionPerformed(ActionEvent e) {
			stopEdit();
		}
	}

	class DefaultListCellRenderer implements ListCellRenderer {

		private JTextField tf;

		@Override
		public Component getListCellRendererComponent(CList0 list, int index) {
			boolean isSelected = list.isSelected(index);
			Object value = list.getElementAt(index);
			boolean isMouseOn = list.isMouseOn(index);
			String valueStr = value == null ? "" : value.toString();
			boolean isEditing = editingIndex == index;
			if (isEditable() && isEditing) {
				if (tf == null) {
					tf = new JTextField();
					tf.addActionListener(handler);
				}
				tf.setText(valueStr);
				return tf;
			} else {
				JLabel lb = new JLabel();
				lb.setOpaque(true);
				lb.setText(valueStr);

				Color backColor = Color.white;
				if (isSelected) {
					backColor = Color.cyan.darker();
				} else if (isMouseOn) {
					backColor = Color.cyan;
				}
				lb.setBackground(backColor);
				lb.setForeground(isSelected ? Color.white : Color.black);

				return lb;
			}
		}
	}
}
